// idb.filesystem.js is required for Firefox because it doesn't support `requestFileSystem` and `webkitRequestFileSystem`
import 'idb.filesystem.js'

export type ReadFileType = 'ArrayBuffer' | 'BinaryString' | 'DataURL' | 'Text'


export type Entry = {
  dir: string;
  name: string;
  relativePath: string;
  fullPath: string;
  size: number;
  isFile: boolean;
  isDirectory: boolean;
  lastModified: Date;
}

export interface IBrowserFileSystem {
  list: (dir: string) => Promise<Entry[]>;
  readFile: (filePath: string, type: ReadFileType) => Promise<string | ArrayBuffer | null>;
  writeFile: (filePath: string, blob: Blob, size?: number) => Promise<string>;
  removeFile: (filePath: string) => Promise<void>;
  moveFile: (sourcePath: string, targetPath: string) => Promise<Entry>;
  copyFile: (sourcePath: string, targetPath: string) => Promise<Entry>;
  getDirectory: (dir: string, shouldCreate: boolean, fs?: IBrowserFileSystem) => Promise<DirectoryEntry>;
  getMetadata: (filePath: string | Entry, isDirectory?: boolean) => Promise<Metadata>;
  exists: (path: string, options?: { type: 'file' | 'directory' }) => Promise<boolean>;
  existsStat: (path: string) => Promise<{ isFile: boolean, isDirectory: boolean }>;
  ensureDirectory: (dir: string, fs?: IBrowserFileSystem) => Promise<DirectoryEntry>;
  rmdir: (dir: string, fs?: IBrowserFileSystem) => Promise<void>;
  rmdirR: (dir: string, fs?: IBrowserFileSystem) => Promise<void>;
}

const fs = (function () {
  const requestFileSystem: any = self.requestFileSystem || self.webkitRequestFileSystem

  if (!requestFileSystem) {
    console.warn('requestFileSystem not supported')
    return undefined
  }

  const dumbSize = 1024 * 1024
  const maxSize = 5 * 1024 * 1024

  const getFS = (size?: number) => {
    size = size || maxSize

    return new Promise((resolve, reject) => {
      requestFileSystem(window.TEMPORARY, size, resolve, reject)
    })
  }

  // 获取目录
  const getDirectory = (dir: string | string[], shouldCreate?: boolean, fs?: any): Promise<DirectoryEntry> => {
    const parts = (Array.isArray(dir) ? dir : dir.split('/')).filter(p => p && p.length)
    const getDir: any = (parts: any[], directoryEntry: any) => {
      if (!parts || !parts.length) return Promise.resolve(directoryEntry)

      return new Promise((resolve, reject) => {
        directoryEntry.getDirectory(
          parts[0],
          { create: !!shouldCreate },
          (dirEntry: any) => resolve(dirEntry),
          (e: any) => reject(e)
        )
      }).then(entry => getDir(parts.slice(1), entry))
    }

    const pFS = fs ? Promise.resolve(fs) : getFS(dumbSize)
    return pFS.then((fs: any) => getDir(parts, fs.root))
  }

  // 确保目录存在(创建目录)
  const ensureDirectory = (dir: string, fs?: any): Promise<DirectoryEntry> => {
    return getDirectory(dir, true, fs)
  }

  // 删除目录
  const rmdir = (dir: string, fs?: any): Promise<void> => {
    return getDirectory(dir, false, fs)
      .then((directoryEntry) => {
        return new Promise((resolve, reject) => {
          directoryEntry.remove(resolve, reject)
        })
      })
  }
  // 递归删除
  const rmdirR = (dir: string, fs?: any): Promise<void> => {
    return getDirectory(dir, false, fs)
      .then((directoryEntry) => {
        return new Promise((resolve, reject) => {
          return directoryEntry.removeRecursively(resolve, reject)
        })
      })
  }

  // @return a Promise of [FileSystemEntries]
  // 列表
  const list = (dir: string = '/'): Promise<any> => {
    return getFS(dumbSize)
      .then((fs) => {
        return new Promise((resolve, reject) => {
          getDirectory(dir).then(dirEntry => {
            let result: any = []
            const dirReader = dirEntry.createReader()
            const read = () => {
              dirReader.readEntries(
                (entries: any) => {
                  if (entries.length === 0) {
                    resolve(result.sort())
                  } else {
                    result = result.concat(Array.from(entries))
                    read()
                  }
                },
                reject)
            }

            read()
          }).catch(reject)
        })
      })
      .catch(e => {
        console.warn('list', e.code, e.name, e.message)
        throw e
      })
  }

  // 文件或目录得所在路径（父级）及名称（自身）
  const fileLocator = (filePath: string, fs: any) => {
    const parts = filePath.split('/')
    return getDirectory(parts.slice(0, -1), false, fs)
      .then(directoryEntry => ({
        directoryEntry,
        fileName: parts.slice(-1)[0]
      }))
  }

  // 读文件
  const readFile = (filePath: string, type: ReadFileType): Promise<any> => {
    if (['ArrayBuffer', 'BinaryString', 'DataURL', 'Text'].indexOf(type) === -1) {
      throw new Error(`invalid readFile type, '${type}'`)
    }

    return getFS()
      .then(fs => {
        return fileLocator(filePath, fs)
          .then(({ directoryEntry, fileName }) => {
            return new Promise((resolve, reject) => {
              directoryEntry.getFile(fileName, {}, (fileEntry: any) => {
                fileEntry.file((file: any) => {
                  const reader = new FileReader()

                  reader.onerror = reject
                  reader.onloadend = function () {
                    resolve(this.result)
                  }

                  switch (type) {
                    case 'ArrayBuffer': return reader.readAsArrayBuffer(file)
                    case 'BinaryString': return reader.readAsBinaryString(file)
                    case 'DataURL': return reader.readAsDataURL(file)
                    case 'Text': return reader.readAsText(file)
                    default: throw new Error(`unsupported data type, '${type}`)
                  }
                }, reject)
              }, reject)
            })
          })
      })
      .catch(e => {
        console.warn('readFile', e.code, e.name, e.message)
        throw e
      })
  }

  // 写文件
  const writeFile = (filePath: string, blob: Blob, size?: number): Promise<any> => {
    return getFS(size)
      .then(fs => {
        return fileLocator(filePath, fs)
          .then(({ directoryEntry, fileName }) => {
            return new Promise((resolve, reject) => {
              directoryEntry.getFile(fileName, { create: true }, (fileEntry: any) => {
                fileEntry.createWriter((fileWriter: any) => {
                  fileWriter.onwriteend = () => resolve(fileEntry.toURL())
                  fileWriter.onerror = reject

                  fileWriter.write(blob)
                })
              }, reject)
            })
          })
      })
      .catch(e => {
        console.warn(e.code, e.name, e.message)
        throw e
      })
  }

  // 删除文件
  const removeFile = (filePath: string): Promise<any> => {
    return getFS()
      .then(fs => {
        return fileLocator(filePath, fs)
          .then(({ directoryEntry, fileName }) => {
            return new Promise((resolve: any, reject: any) => {
              directoryEntry.getFile(fileName, { create: true }, (fileEntry: any) => {
                fileEntry.remove(resolve, reject)
              }, reject)
            })
          })
      })
      .catch(e => {
        console.warn('removeFile', e.code, e.name, e.message)
        throw e
      })
  }

  // 移动文件
  const moveFile = (srcPath: string, targetPath: string): Promise<Entry> => {
    return getFS()
      .then(fs => {
        return Promise.all([
          fileLocator(srcPath, fs),
          fileLocator(targetPath, fs)
        ])
          .then(tuple => {
            const srcDirEntry = tuple[0].directoryEntry
            const srcFileName = tuple[0].fileName
            const tgtDirEntry = tuple[1].directoryEntry
            const tgtFileName = tuple[1].fileName

            return new Promise((resolve, reject) => {
              srcDirEntry.getFile(srcFileName, {}, (fileEntry: any) => {
                try {
                  fileEntry.moveTo(tgtDirEntry, tgtFileName, resolve, reject)
                } catch (e) {
                  // Note: For firefox, we use `idb.filesystem.js`, but it hasn't implemented `moveTo` method
                  // so we have to mock it with read / write / remove
                  readFile(srcPath, 'ArrayBuffer')
                    .then((arrayBuffer: any) => writeFile(targetPath, new Blob([new Uint8Array(arrayBuffer)])))
                    .then(() => removeFile(srcPath))
                    .then(resolve, reject)
                }
              }, reject)
            })
          })
      })
  }

  // 复制文件
  const copyFile = (srcPath: string, targetPath: string): Promise<any> => {
    return getFS()
      .then(fs => {
        return Promise.all([
          fileLocator(srcPath, fs),
          fileLocator(targetPath, fs)
        ])
          .then(tuple => {
            const srcDirEntry = tuple[0].directoryEntry
            const srcFileName = tuple[0].fileName
            const tgtDirEntry = tuple[1].directoryEntry
            const tgtFileName = tuple[1].fileName

            return new Promise((resolve, reject) => {
              srcDirEntry.getFile(srcFileName, {}, (fileEntry: any) => {
                try {
                  fileEntry.copyTo(tgtDirEntry, tgtFileName, resolve, reject)
                } catch (e) {
                  // Note: For firefox, we use `idb.filesystem.js`, but it hasn't implemented `copyTo` method
                  // so we have to mock it with read / write
                  readFile(srcPath, 'ArrayBuffer')
                    .then((arrayBuffer: any) => writeFile(targetPath, new Blob([new Uint8Array(arrayBuffer)])))
                    .then(resolve, reject)
                }
              }, reject)
            })
          })
      })
      .catch(e => {
        console.warn('copyFile', e.code, e.name, e.message)
        throw e
      })
  }

  // 获取文件元数据（size和modificationTime）
  const getMetadata = (filePath: any, isDirectory: boolean = false): Promise<any> => {
    return getFS()
      .then(fs => {
        if (filePath.getMetadata) {
          return new Promise((resolve, reject) => {
            return filePath.getMetadata(resolve)
          })
        }

        return fileLocator(filePath, fs)
          .then(({ directoryEntry, fileName }) => {
            return new Promise((resolve, reject) => {
              const args: any = [
                fileName,
                { create: false },
                (entry: any) => {
                  entry.getMetadata(resolve)
                },
                reject
              ]

              if (isDirectory) {
                directoryEntry.getDirectory(...args)
              } else {
                directoryEntry.getFile(...args)
              }
            })
          })
      })
      .catch(e => {
        console.warn('getMetadata', e.code, e.name, e.message)
        throw e
      })
  }

  // 提供2个目录或文件是否存在得方法
  const existsStat = (entryPath: string): Promise<any> => {
    return getFS()
      .then(fs => {
        return fileLocator(entryPath, fs)
          .then(({ directoryEntry, fileName }: any) => {
            const isSomeEntry = (getMethodName: any) => {
              return new Promise((resolve) => {
                directoryEntry[getMethodName](
                  fileName,
                  { create: false },
                  () => {
                    resolve(true)
                  },
                  () => resolve(false)
                )
              })
            }

            const pIsFile = isSomeEntry('getFile') // 从目录中查找文件
            const pIsDir = isSomeEntry('getDirectory') // 从目录中查找目录

            return Promise.all([pIsFile, pIsDir])
              .then(([isFile, isDirectory]) => {
                return {
                  isFile,
                  isDirectory
                }
              })
          })
      })
      .catch(e => {
        // DOMException.NOT_FOUND_ERR === 8
        if (e && e.code === 8) {
          return {
            isFile: false,
            isDirectory: false
          }
        }

        console.warn('fs.exists', e.code, e.name, e.message)
        throw e
      })
  }

  // 是否存在
  const exists = (entryPath: string, { type }: { type?: 'file' | 'directory' } = {}): Promise<boolean> => {
    return existsStat(entryPath)
      .then((stat) => {
        switch (type) {
          case 'file':
            return stat.isFile

          case 'directory':
            return stat.isDirectory

          default:
            return stat.isFile || stat.isDirectory
        }
      })
  }

  return {
    list,
    readFile,
    writeFile,
    removeFile,
    moveFile,
    copyFile,
    getDirectory,
    getMetadata,
    ensureDirectory,
    exists,
    existsStat,
    rmdir,
    rmdirR
  }
})()

// 提供类似 node:fs/promise的库
export default fs

// webkit:fs/promise

// 替换方案
// https://developer.mozilla.org/zh-CN/docs/Web/API/File_System_API/Origin_private_file_system#%E5%9C%A8_web_worker_%E4%B8%AD%E6%93%8D%E4%BD%9C_opfs


// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/showOpenFilePicker